<script setup lang="ts">
import type {Ref} from 'vue'
import {computed, onMounted, onUnmounted, ref} from 'vue'
import {useRoute} from 'vue-router'
import {storeToRefs} from 'pinia'
import {NAutoComplete, NButton, NInput, useDialog, useMessage} from 'naive-ui'
import html2canvas from 'html2canvas'
import {Message} from './components'
import {useScroll} from './hooks/useScroll'
import {useChat} from './hooks/useChat'
import {useCopyCode} from './hooks/useCopyCode'
import {useUsingContext} from './hooks/useUsingContext'
import HeaderComponent from './components/Header/index.vue'
import {HoverButton, SvgIcon} from '@/components/index'
import {useBasicLayout} from '@/hooks/useBasicLayout'
import {useChatStore, usePromptStore} from '@/store'
import {t} from '@/locales'
import {chatMessageApi} from "@/api/chat_message";

let controller = new AbortController()

const route = useRoute()
const dialog = useDialog()
const ms = useMessage()

const chatStore = useChatStore()

useCopyCode()

const {isMobile} = useBasicLayout()
const {
	addChat,
	updateChat,
	updateChatSome,
	getChatByUuidAndIndex
} = useChat()
const {
	scrollRef,
	scrollToBottom,
	scrollToBottomIfAtBottom
} = useScroll()
const {
	usingContext,
	toggleUsingContext
} = useUsingContext()

const {uuid} = route.params as { uuid: string }

const dataSources = computed(() => chatStore.getChatByUuid(+uuid))
const conversationList = computed(() => dataSources.value.filter(item => (!item.inversion && !item.error)))

const prompt = ref<string>('')
const loading = ref<boolean>(false)
const inputRef = ref<Ref | null>(null)

// 添加PromptStore
const promptStore = usePromptStore()

// 使用storeToRefs，保证store修改后，联想部分能够重新渲染
const {promptList: promptTemplate} = storeToRefs<any>(promptStore)

// 未知原因刷新页面，loading 状态不会重置，手动重置
dataSources.value.forEach((item, index) => {
	if (item.loading) {
		updateChatSome(+uuid, index, {loading: false})
	}
})

/**
 * @description: 提交对话框中的内容
 * @return {*}
 */
function handleSubmit() {
	onConversation()
}

/**
 * @description: 生成这句对话
 * @return {*}
 */
async function onConversation() {
	const message = prompt.value

	if (loading.value) {
		return
	}

	if (!message || message.trim() === '') {
		return
	}

	controller = new AbortController()

	addChat(
		+uuid,
		{
			dateTime: new Date().toLocaleString(),
			text: message,
			inversion: true,
			error: false,
			conversationOptions: null,
			requestOptions: {
				prompt: message,
				options: null
			},
		},
	)
	await scrollToBottom()

	loading.value = true
	prompt.value = ''

	let options: Chat.ConversationRequest = {}
	const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions

	if (lastContext && usingContext.value) {
		options = {...lastContext}
	}

	addChat(
		+uuid,
		{
			dateTime: new Date().toLocaleString(),
			text: '',
			loading: true,
			inversion: false,
			error: false,
			conversationOptions: null,
			requestOptions: {
				prompt: message,
				options: {...options}
			},
		},
	)
	await scrollToBottom()

	try {
		const lastText = ''
		const fetchChatAPIOnce = async () => {
			await chatMessageApi.send({
				prompt: message,
				options,
				signal: controller.signal,
				onDownloadProgress: ({event}) => {
					const xhr = event.target
					let {responseText} = xhr
					try {
						let json = chatMessageApi.parseChunk(responseText)
						updateChat(
							+uuid,
							dataSources.value.length - 1,
							{
								dateTime: new Date().toLocaleString(),
								text: lastText + (json.text ?? ''),
								inversion: false,
								error: false,
								loading: true,
								conversationOptions: {
									conversationId: json.conversationId,
									parentMessageId: json.id
								},
								requestOptions: {
									prompt: message,
									options: {...options}
								},
							},
						)

						scrollToBottomIfAtBottom()
					} catch (error) {
						console.log(error)
					}
				},
			})
			updateChatSome(+uuid, dataSources.value.length - 1, {loading: false})
		}

		await fetchChatAPIOnce()
	} catch (error: any) {
		const errorMessage = error?.message ?? t('common.wrong')

		if (error.message === 'canceled') {
			updateChatSome(
				+uuid,
				dataSources.value.length - 1,
				{
					loading: false,
				},
			)
			await scrollToBottomIfAtBottom()
			return
		}

		const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)

		if (currentChat?.text && currentChat.text !== '') {
			updateChatSome(
				+uuid,
				dataSources.value.length - 1,
				{
					text: `${currentChat.text}\n[${errorMessage}]`,
					error: false,
					loading: false,
				},
			)
			return
		}

		updateChat(
			+uuid,
			dataSources.value.length - 1,
			{
				dateTime: new Date().toLocaleString(),
				text: errorMessage,
				inversion: false,
				error: true,
				loading: false,
				conversationOptions: null,
				requestOptions: {
					prompt: message,
					options: {...options}
				},
			},
		)
		await scrollToBottomIfAtBottom()
	} finally {
		loading.value = false
	}
}

/**
 * @description: 再次生成这句对话
 * @param {*} index
 * @return {*}
 */
async function onRegenerate(index: number) {
	if (loading.value) {
		return
	}

	controller = new AbortController()

	const {requestOptions} = dataSources.value[index]

	const message = requestOptions?.prompt ?? ''

	let options: Chat.ConversationRequest = {}

	if (requestOptions.options) {
		options = {...requestOptions.options}
	}

	loading.value = true

	updateChat(
		+uuid,
		index,
		{
			dateTime: new Date().toLocaleString(),
			text: '',
			inversion: false,
			error: false,
			loading: true,
			conversationOptions: null,
			requestOptions: {
				prompt: message,
				options: {...options}
			},
		},
	)

	try {
		const lastText = ''
		const fetchChatAPIOnce = async () => {
			await chatMessageApi.send({
				prompt: message,
				options,
				signal: controller.signal,
				onDownloadProgress: ({event}) => {
					const xhr = event.target
					let {responseText} = xhr
					// 总是处理最后一行 TODO
					try {
						// let json = JSON.parse(JSON.parse(chunk));
						let json = chatMessageApi.parseChunk(responseText)
						updateChat(
							+uuid,
							index,
							{
								dateTime: new Date().toLocaleString(),
								text: lastText + (json.text ?? ''),
								inversion: false,
								error: false,
								loading: true,
								conversationOptions: {
									conversationId: json.conversationId,
									parentMessageId: json.id
								},
								requestOptions: {
									prompt: message,
									options: {...options}
								},
							},
						)
					} catch (error) {
						console.log(error)
					}
				},
			})
			await updateChatSome(+uuid, index, {loading: false})
		}
		await fetchChatAPIOnce()
	} catch (error: any) {
		if (error.message === 'canceled') {
			await updateChatSome(
				+uuid,
				index,
				{
					loading: false,
				},
			)
			return
		}

		const errorMessage = error?.message ?? t('common.wrong')

		updateChat(
			+uuid,
			index,
			{
				dateTime: new Date().toLocaleString(),
				text: errorMessage,
				inversion: false,
				error: true,
				loading: false,
				conversationOptions: null,
				requestOptions: {
					prompt: message,
					options: {...options}
				},
			},
		)
	} finally {
		loading.value = false
	}
}

/**
 * @description: 保存会话到图片
 * @return {*}
 */
function handleExport() {
	if (loading.value) {
		return
	}

	const d = dialog.warning({
		title: t('chat.exportImage'),
		content: t('chat.exportImageConfirm'),
		positiveText: t('common.yes'),
		negativeText: t('common.no'),
		onPositiveClick: async () => {
			try {
				d.loading = true
				const ele = document.getElementById('image-wrapper')
				const canvas = await html2canvas(ele as HTMLDivElement, {
					useCORS: true,
				})
				const imgUrl = canvas.toDataURL('image/png')
				const tempLink = document.createElement('a')
				tempLink.style.display = 'none'
				tempLink.href = imgUrl
				tempLink.setAttribute('download', 'chat-shot.png')
				if (typeof tempLink.download === 'undefined') {
					tempLink.setAttribute('target', '_blank')
				}

				document.body.appendChild(tempLink)
				tempLink.click()
				document.body.removeChild(tempLink)
				window.URL.revokeObjectURL(imgUrl)
				d.loading = false
				ms.success(t('chat.exportSuccess'))
				await Promise.resolve()
			} catch (error: any) {
				ms.error(t('chat.exportFailed'))
			} finally {
				d.loading = false
			}
		},
	})
}

/**
 * @description: 删除当前消息
 * @param {*} index
 * @return {*}
 */
function handleDelete(index: number) {
	if (loading.value) {
		return
	}

	dialog.warning({
		title: t('chat.deleteMessage'),
		content: t('chat.deleteMessageConfirm'),
		positiveText: t('common.yes'),
		negativeText: t('common.no'),
		onPositiveClick: () => {
			chatStore.deleteChatByUuid(+uuid, index)
		},
	})
}

/**
 * @description: 清除聊天记录
 * @return {*}
 */
function handleClear() {
	if (loading.value) {
		return
	}

	dialog.warning({
		title: t('chat.clearChat'),
		content: t('chat.clearChatConfirm'),
		positiveText: t('common.yes'),
		negativeText: t('common.no'),
		onPositiveClick: () => {
			chatStore.clearChatByUuid(+uuid)
		},
	})
}

/**
 * @description: 提交对话框中的内容
 * @return {*}
 */
function handleEnter(event: KeyboardEvent) {
	if (!isMobile.value) {
		if (event.key === 'Enter' && !event.shiftKey) {
			event.preventDefault()
			handleSubmit()
		}
	} else {
		if (event.key === 'Enter' && event.ctrlKey) {
			event.preventDefault()
			handleSubmit()
		}
	}
}

/**
 * @description: 停止生成回答
 * @return {*}
 */
function handleStop() {
	if (loading.value) {
		controller.abort()
		loading.value = false
	}
}

// 可优化部分
// 搜索选项计算，这里使用value作为索引项，所以当出现重复value时渲染异常(多项同时出现选中效果)
// 理想状态下其实应该是key作为索引项,但官方的renderOption会出现问题，所以就需要value反renderLabel实现
/**
 * @description: TODO 提示词?
 * @param {*} computed
 * @return {*}
 */
const searchOptions = computed(() => {
	if (prompt.value.startsWith('/')) {
		return promptTemplate.value.filter((item: {
			key: string
		}) => item.key.toLowerCase().includes(prompt.value.substring(1).toLowerCase())).map((obj: { value: any }) => {
			return {
				label: obj.value,
				value: obj.value,
			}
		})
	} else {
		return []
	}
})

// value反渲染key
const renderOption = (option: { label: string }) => {
	for (const i of promptTemplate.value) {
		if (i.value === option.label) {
			return [i.key]
		}
	}
	return []
}

const placeholder = computed(() => {
	if (isMobile.value) {
		return t('chat.placeholderMobile')
	}
	return t('chat.placeholder')
})

const buttonDisabled = computed(() => {
	return loading.value || !prompt.value || prompt.value.trim() === ''
})

const footerClass = computed(() => {
	let classes = ['p-4']
	if (isMobile.value) {
		classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden']
	}
	return classes
})

onMounted(() => {
	scrollToBottom()
	if (inputRef.value && !isMobile.value) {
		inputRef.value?.focus()
	}
})

onUnmounted(() => {
	if (loading.value) {
		controller.abort()
	}
})
</script>

<template>
	<div class="flex flex-col w-full h-full">
		<HeaderComponent
			v-if="isMobile"
			:using-context="usingContext"
			@export="handleExport"
			@toggle-using-context="toggleUsingContext"
		/>
		<main class="flex-1 overflow-hidden">
			<div id="scrollRef" ref="scrollRef" class="h-full overflow-hidden overflow-y-auto">
				<div
					id="image-wrapper"
					class="w-full max-w-screen-xl m-auto dark:bg-[#101014]"
					:class="[isMobile ? 'p-2' : 'p-4']"
				>
					<!-- 背景提示框 -->
					<template v-if="!dataSources.length">
						<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
							<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl"/>
							<span>Aha~</span>
						</div>
					</template>
					<!-- 消息列表 -->
					<template v-else>
						<div>
							<Message
								v-for="(item, index) of dataSources"
								:key="index"
								:date-time="item.dateTime"
								:text="item.text"
								:inversion="item.inversion"
								:error="item.error"
								:loading="item.loading"
								@regenerate="onRegenerate(index)"
								@delete="handleDelete(index)"
							/>
							<!-- 停止生成 -->
							<div class="sticky bottom-0 left-0 flex justify-center">
								<NButton v-if="loading" type="warning" @click="handleStop">
									<template #icon>
										<SvgIcon icon="ri:stop-circle-line"/>
									</template>
									停止生成
								</NButton>
							</div>
						</div>
					</template>
				</div>
			</div>
		</main>
		<footer :class="footerClass">
			<div class="w-full max-w-screen-xl m-auto">
				<div class="flex items-center justify-between space-x-2">
					<HoverButton tooltip="删除记录" @click="handleClear">
            <span class="text-xl text-[#4f555e] dark:text-white">
              <SvgIcon icon="ri:delete-bin-line"/>
            </span>
					</HoverButton>
					<HoverButton v-if="!isMobile" tooltip="保存会话到图片" @click="handleExport">
            <span class="text-xl text-[#4f555e] dark:text-white">
              <SvgIcon icon="ri:download-2-line"/>
            </span>
					</HoverButton>
					<HoverButton v-if="!isMobile" tooltip="切换聊天模式" @click="toggleUsingContext">
            <span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
              <SvgIcon icon="ri:chat-history-line"/>
            </span>
					</HoverButton>
					<NAutoComplete v-model:value="prompt" :options="searchOptions" :render-label="renderOption">
						<template #default="{ handleInput, handleBlur, handleFocus }">
							<NInput
								ref="inputRef"
								v-model:value="prompt"
								type="textarea"
								:placeholder="placeholder"
								:autosize="{ minRows: 1, maxRows: isMobile ? 4 : 8 }"
								@input="handleInput"
								@focus="handleFocus"
								@blur="handleBlur"
								@keypress="handleEnter"
							/>
						</template>
					</NAutoComplete>
					<NButton type="primary" :disabled="buttonDisabled" @click="handleSubmit">
						<template #icon>
              <span class="dark:text-black">
                <SvgIcon icon="ri:send-plane-fill"/>
              </span>
						</template>
					</NButton>
				</div>
			</div>
		</footer>
	</div>
</template>
